package jdbmaplite.mapper.impl;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import jdbmaplite.manager.ParseConfigException;
import jdbmaplite.mapper.FormatSqlException;
import jdbmaplite.mapper.SqlDefinition;
import jdbmaplite.mapper.SqlDefinitionFactory;
import jdbmaplite.util.ClassUtils;
import jdbmaplite.util.DBUtils;
import jdbmaplite.util.LogUtils;
import jdbmaplite.util.MethodInfo;
import jdbmaplite.util.StringUtils;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class JDBMapLiteSqlDefinitionFactory implements SqlDefinitionFactory {

	private static final Log _cat = LogFactory
			.getLog(JDBMapLiteSqlDefinitionFactory.class);

	private String configFilePath = null;;
	private boolean isConfigFilePathAbsolutePath = false;
	private Map<String, SqlDefinition> sqlDefinitionPool = null;
	private Map<String, MethodInfo> methodInfoPool = null;

	/**
	 * Default constructor.
	 * 
	 * @param configFilePath
	 *            String
	 * @param isConfigFilePathAbsolutePath
	 *            boolean
	 * @throws ParseConfigException
	 */
	public JDBMapLiteSqlDefinitionFactory(String configFilePath,
			boolean isConfigFilePathAbsolutePath) throws ParseConfigException {
		super();
		loadSqlDefinition(configFilePath, isConfigFilePathAbsolutePath);
	}

	public void reloadSqlDefinition() throws ParseConfigException {
		loadSqlDefinition(configFilePath, isConfigFilePathAbsolutePath);
	}

	public SqlDefinition getSqlDefinition(String elementId) {
		return sqlDefinitionPool.get(elementId);
	}

	public MethodInfo getMethodInfo(Class<?> executedBy, int shiftTraceCount,
			Object... parameters) throws IOException {
		if (executedBy == null) {
			throw new IllegalArgumentException("executedBy cannot be null!");
		}
		// shiftTraceElement=3, skip until Dao class
		StackTraceElement caller = ClassUtils.traceCaller(shiftTraceCount + 3);
		if (caller == null) {
			throw new RuntimeException("caller not found!");
		}
		String methodInfoPoolKey = caller.getClassName()
				+ caller.getLineNumber();
		MethodInfo methodInfo = methodInfoPool.get(methodInfoPoolKey);
		if (methodInfo == null) {
			synchronized (methodInfoPool) {
				methodInfo = ClassUtils.getDeclaredMethodInfo(executedBy,
						caller.getMethodName(), parameters);
				methodInfoPool.put(methodInfoPoolKey, methodInfo);
			}
		}
		return methodInfo;
	}

	public String formatSql(SqlDefinition sqlDef, String[] parameterNames,
			Object... parameterValues) throws FormatSqlException {
		if (sqlDef == null) {
			throw new FormatSqlException("Sql definition not found!");
		}
		String sql = sqlDef.getSql();
		int parametersSize = 0;
		if (parameterNames == null || parameterValues == null
				|| (parametersSize = parameterNames.length) <= 0
				|| parametersSize != parameterValues.length) {
			return sql;
		}
		/* escape sql */
		for (int i = 0; i < parametersSize; i++) {
			Object parameter = parameterValues[i];
			if (parameter == null) {
				continue;
			}
			if (parameter instanceof CharSequence
					|| parameter instanceof Character) {
				parameterValues[i] = StringEscapeUtils.escapeSql(parameter
						.toString());
			}
		}
		/* format sql */
		return StringUtils.format(sql, "$", parameterNames, parameterValues);
	}

	public String formatSqlByObject(SqlDefinition sqlDef, Object object)
			throws FormatSqlException {
		if (sqlDef == null) {
			throw new FormatSqlException("Sql definition not found!");
		}
		String sql = sqlDef.getSql();
		String[] typeFieldNames = ClassUtils.getDeclaredFieldNames(object
				.getClass());
		Object[] typeFieldValues = null;
		try {
			typeFieldValues = ClassUtils.getDeclaredFieldValues(object);
		} catch (Exception e) {
			throw new FormatSqlException(
					"Get type field values from sql definition failed!");
		}
		int typeFieldValuesSize = 0;
		if (typeFieldNames == null || typeFieldValues == null
				|| typeFieldNames.length <= 0
				|| (typeFieldValuesSize = typeFieldValues.length) <= 0) {
			return sql;
		}
		/* escape sql */
		for (int i = 0; i < typeFieldValuesSize; i++) {
			Object typeFieldValue = typeFieldValues[i];
			if (typeFieldValue == null) {
				continue;
			}
			if (typeFieldValue instanceof CharSequence
					|| typeFieldValue instanceof Character) {
				typeFieldValues[i] = StringEscapeUtils.escapeSql(String
						.valueOf(typeFieldValue));
			}
		}
		/* format sql */
		return StringUtils
				.formatEach(sql, "$", typeFieldNames, typeFieldValues);
	}

	public Object convertResultValue(ResultSet rs, int columnIndex,
			Class<?> mappingType) throws SQLException {
		if (mappingType.isPrimitive()) {
			if (mappingType == boolean.class) {
				return rs.getInt(columnIndex);
			} else if (mappingType == byte.class) {
				return rs.getByte(columnIndex);
			} else if (mappingType == char.class) {
				String result = rs.getString(columnIndex);
				if (result != null && result.length() > 0) {
					return result.charAt(0);
				}
				return Character.MIN_VALUE;
			} else if (mappingType == double.class) {
				return rs.getDouble(columnIndex);
			} else if (mappingType == float.class) {
				return rs.getFloat(columnIndex);
			} else if (mappingType == int.class) {
				return rs.getInt(columnIndex);
			} else if (mappingType == long.class) {
				return rs.getLong(columnIndex);
			} else if (mappingType == short.class) {
				return rs.getShort(columnIndex);
			}
			throw new SQLException("Unsupport type");
		} else if (mappingType == byte[].class) {
			return rs.getBytes(columnIndex);
		}
		Object result = rs.getObject(columnIndex);
		if (result == null) {
			return null;
		}
		if (mappingType.isInstance(result)) {
			return result;
		}
		try {
			if (result instanceof Number) {
				Number number = (Number) result;
				if (mappingType.isAssignableFrom(Byte.class)) {
					return number.byteValue();
				} else if (mappingType.isAssignableFrom(Double.class)) {
					return number.doubleValue();
				} else if (mappingType.isAssignableFrom(Float.class)) {
					return number.floatValue();
				} else if (mappingType.isAssignableFrom(Integer.class)) {
					return number.intValue();
				} else if (mappingType.isAssignableFrom(Long.class)) {
					return number.longValue();
				} else if (mappingType.isAssignableFrom(Short.class)) {
					return number.shortValue();
				}
				throw new ClassCastException("Unsupport type");
			}
			return mappingType.cast(result);
		} catch (ClassCastException e) {
			_cat.warn(LogUtils.formatDebugLog("Convert value",
					"class=%s, value=%s, castTo=%s", result.getClass(), result,
					mappingType), e);
			throw e;
		}
	}

	/**
	 * Load Sql Definition
	 * 
	 * @param configFilePath
	 *            String
	 * @param isConfigFilePathAbsolutePath
	 *            boolean
	 * @throws ParseConfigException
	 */
	public void loadSqlDefinition(String configFilePath,
			boolean isConfigFilePathAbsolutePath) throws ParseConfigException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		InputStream is = null;
		try {
			is = (isConfigFilePathAbsolutePath ? new FileInputStream(
					configFilePath) : getClass().getResourceAsStream(
					configFilePath));
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document sqlDaoDocument = builder.parse(is);
			// sqlDao
			Element sqlDao = sqlDaoDocument.getDocumentElement();
			// init
			NodeList sqlList = sqlDao.getElementsByTagName("sql");
			synchronized (this) {
				sqlDefinitionPool = new ConcurrentHashMap<String, SqlDefinition>();
				methodInfoPool = new ConcurrentHashMap<String, MethodInfo>();
				for (int i = 0, sqlListSize = sqlList.getLength(); i < sqlListSize; i++) {
					Element sqlNode = (Element) sqlList.item(i);
					String sqlNodeId = sqlNode.getAttribute("id");
					String sqlContent = sqlNode.getTextContent();
					if (StringUtils.isAnyEmpty(sqlNodeId, sqlContent)) {
						continue;
					}
					sqlContent = sqlContent.trim();
					// cache it
					sqlDefinitionPool.put(sqlNodeId, new SqlDefinition(
							sqlNodeId, sqlContent));
				}
				this.configFilePath = configFilePath;
				this.isConfigFilePathAbsolutePath = isConfigFilePathAbsolutePath;
			}
		} catch (Throwable e) {
			_cat.warn("Parse conifg failed!", e);
			throw new ParseConfigException(e);
		} finally {
			DBUtils.closeQuietly(is);
		}
	}

	/**
	 * Get Sql Definition Pool
	 * 
	 * @return Map{String, SqlDefinition}
	 */
	public Map<String, SqlDefinition> getSqlDefinitionPool() {
		return sqlDefinitionPool;
	}

	/**
	 * Set SqlDefinition Pool
	 * 
	 * @param sqlDefinitionPool
	 *            Map{String, SqlDefinition}
	 */
	public void setSqlDefinitionPool(
			Map<String, SqlDefinition> sqlDefinitionPool) {
		this.sqlDefinitionPool = sqlDefinitionPool;
	}

	/**
	 * Get Method Info Pool
	 * 
	 * @return Map{String, MethodInfo}
	 */
	public Map<String, MethodInfo> getMethodInfoPool() {
		return methodInfoPool;
	}

	/**
	 * Set Method Info Pool
	 * 
	 * @param methodInfoPool
	 *            Map{String, MethodInfo}
	 */
	public void setMethodInfoPool(Map<String, MethodInfo> methodInfoPool) {
		this.methodInfoPool = methodInfoPool;
	}

}